<template>
  <article class="page page-jssfres-download">
    <h4 class="text-center">京师书法课件资源下载器</h4>
    <form class="form">
      <div class="alert alert-info">
        <div>资源下载完成后会生成一个以“课本ID”命名的文件夹，使用时，需先将该文件夹改名为“coursewares”，然后拷贝到京师书法程序目录下的“resources”文件夹中即可。</div>
        <div>使用磁盘证书，可限制程序只能在该磁盘下运行（需与特定京师书法程序版本配套使用）。</div>
      </div>
      <div class="form-group">
        <fieldset>
          <legend>参数设置</legend>
          <div class="row-flex" align="center">
            <div class="row-flex-prefix">课本ID</div>
            <div class="row-flex-main"><input class="form-control" type="text" v-model="bookId" /></div>
            <div class="row-flex-suffix">
              <x-button :loading="loadingCount > 0" @click="downloadHandler">下载</x-button>
            </div>
          </div>
          <div class="row-flex" align="center">
            <div class="row-flex-prefix">授课模式</div>
            <div class="row-flex-main text-left">
              <div class="form-check form-check-inline" v-for="(v, i) in teachModeList" :key="i">
                <input class="form-check-input" type="radio" :value="v.value" v-model="teachMode">
                <label class="form-check-label">{{ v.label }}</label>
              </div>
            </div>
            <div class="row-flex-suffix" style="width: 8em;"></div>
          </div>
          <div class="row-flex" align="center">
            <div class="row-flex-prefix">Token</div>
            <div class="row-flex-main"><input class="form-control" type="text" v-model="token" :disabled="tokenLocked" /></div>
            <div class="row-flex-suffix">
              <x-button @click="tokenLocked=!tokenLocked">{{tokenLocked ? '解锁' : '锁定'}}</x-button>
            </div>
          </div>
          <div class="row-flex" align="center">
            <div class="row-flex-prefix">磁盘证书</div>
            <div class="row-flex-main">
              <!-- <select class="form-control" v-model="uDisk" placeholder="请选择目标盘符">
                <option v-for="(v,i) in disksList" :key="i" :value="v.value">{{v.label}}</option>
              </select> -->
              <div class="input-group">
                <div class="input-group-prepend">
                  <select class="form-control" v-model="uDecode" placeholder="编码">
                    <option v-for="(v,i) in decodesList" :key="i" :value="v.value">{{v.label}}</option>
                  </select>
                </div>
                <select class="form-control" v-model="uDisk" placeholder="请选择目标盘符">
                  <option v-for="(v,i) in disksList" :key="i" :value="v.value">{{v.label}}</option>
                </select>
                <div class="input-group-append" @click="initDisksList">
                  <span class="input-group-text">刷新</span>
                </div>
              </div>
            </div>
            <div class="row-flex-main">
              <input class="form-control" type="text" v-model="uDate" placeholder="生产日期，如 2025-02-28" />
            </div>
            <div class="row-flex-suffix">
              <x-button @click="writeDiskDisc">生成</x-button>
            </div>
          </div>
          <div class="row-flex" align="center">
            <div class="row-flex-prefix">保存位置</div>
            <div class="row-flex-main"><input class="form-control" type="text" v-model="toPath" readonly /></div>
            <div class="row-flex-suffix">
              <x-button :loading="loadingCount > 0" @click="setToPath">选择</x-button>
            </div>
          </div>
        </fieldset>
      </div>
      <div class="form-group form-group-log">
        <fieldset>
          <legend>打印日志</legend>
          <div class="fieldset-content">
            <textarea class="form-control" ref="textarea" v-model.trim="result" rows="10" readonly></textarea>
          </div>
        </fieldset>
      </div>
    </form>
  </article>
</template>

<script>
const fs = global.require('fs')
const path = global.require('path')
const {exec} = global.require('child_process')
const iconv = require('iconv-lite')
const {remote} = global.require('electron')
const {dialog} = remote
const selfWindow= remote.getCurrentWindow()
let {ajax, ajaxError, downloadFileV3, readDir, delFile, writeFileSync, jsonParse} = global.jstoolsNode || {}
let _url = {
  getAllFileUrl: 'https://gw.jsxfedu.com/sf-server/book/all-resources', // 获取北师大版课本资源数据下载地址
  getChapterCourseware: 'https://gw.jsxfedu.com/sf-server/book/chapter-courseware', // 获取课本章节及课件
}
let _encryptFiles = [] // 待加密文件列表
let _zipLog = {} // 日志

export default {
  name: 'jssfres-download',
  data () {
    return {
      bookId: '',
      teachMode: 'PROCESS',
      teachModeList: [
        {value: 'PROCESS', label: '流程模式'},
        {value: 'CONCISE', label: '精简模式'}
      ], // 可用的授课模式列表
      token: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJzZiIsImF1ZCI6IlktMSIsImlzcyI6InNmIiwiZXhwIjo0MTAyNDE1OTk5fQ.zZ-4gQ7Ty5xi3ap1TfWeNuMef_wVo-IH-lBn3evfMKc',
      tokenLocked: true,
      toPath: this.$sharedObject('desktopPath'),
      result: '',
      allFileUrl: [],
      chapterCourseware: {},
      uDisk: '',
      uDate: '',
      disksList: [], // 磁盘列表
      uDecode: 'gb2312',
      decodesList: [
        {value: 'utf8', label: 'UTF-8'},
        {value: 'gb2312', label: 'GB2312'}
      ], // 可用的编码列表
      encryptPassword: 'l6fwZHdJbU2Y4jxPDwjoI6P9vltHhc8bvDlExm4vRRg=',
      loadingCount: 0
    }
  },
  watch: {
    result () {
      this.$refs.textarea && (this.$refs.textarea.scrollTop = 10000)
    }
  },
  mounted () {
    this.initDisksList()
  },
  methods: {
    // 初始化磁盘列表
    initDisksList () {
      this.disksList = []
      let command = 'chcp 65001 & wmic logicaldisk get Caption,VolumeSerialNumber,Description /format:list'
      exec(command, {encoding: 'buffer'}, (error, stdout, stderr) => {
        if (error) { return this.$toast('命令执行失败，请以管理员身份运行程序后重试') }
        console.error(`stderr: ${stderr}`)
        let str = iconv.decode(stdout, this.uDecode || 'gb2312') || ''
        let lst = str.match(/.*=.*/g)
        let result = []
        let count = {}
        lst && lst.forEach(v => {
          let arr = v.split('=')
          arr = arr.map(v => (v || '').trim())
          if (!count[arr[0]] && count[arr[0]] !== 0) {count[arr[0]] = 0}
          else {count[arr[0]] = count[arr[0]] + 1}
          let index = count[arr[0]]
          if (!result[index]) {result[index] = {}}
          result[index][arr[0]] = arr[1]
        })
        result = result.map(v => {
          return {
            label: `${v.Caption} (${v.Description})`,
            value: v.VolumeSerialNumber
          }
        })
        this.disksList = result.filter(v => v.value)
      })
    },
    // 设置保存位置
    setToPath () {
      let filePaths = dialog.showOpenDialogSync(selfWindow, {
        title: '选择目录',
        buttonLabel:'确定',
        defaultPath: this.$sharedObject('desktopPath'),
        properties: ['openDirectory']
      })
      if (filePaths instanceof Array && filePaths.length > 0) {
        this.toPath = filePaths[0]
      }
    },
    // 获取课本章节及课件
    getChapterCourseware () {
      this.chapterCourseware = {}
      return ajax.get(_url.getChapterCourseware, {
        params: {bookId: this.bookId, teachingMode: this.teachMode},
        headers: {token: this.token}
      }).then(res => {
        let data = this.ajaxCheck(res)
        this.chapterCourseware = data || {}
      }).catch(err => {
        let msg = ajaxError(err)
        this.result += msg + '\n'
      })
    },
    // 获取北师大版课本资源数据下载地址
    getAllFileUrl () {
      this.allFileUrl = []
      return ajax.get(_url.getAllFileUrl, {
        params: {bookId: this.bookId, teachingMode: this.teachMode},
        headers: {token: this.token}
      }).then(res => {
        let data = this.ajaxCheck(res)
        this.allFileUrl = data || []
        this.result += `查询到 ${this.allFileUrl.length} 个课本资源` + '\n'
      }).catch(err => {
        let msg = ajaxError(err)
        this.result += msg + '\n'
      })
    },
    // 下载所有资源
    downloadAllFile () {
      let _this = this
      let _downloadTasks = []
      this.allFileUrl.forEach(v => {
        let _url = v.split('?')[0] || ''
        let _file = path.basename(_url)
        let _path = path.join(this.toPath, this.bookId, _file)
        let _fileName = _file
        if (_fileName.includes('.')) {
          let _i = _fileName.lastIndexOf('.')
          _fileName = _fileName.substring(0, _i)
        }
        _downloadTasks.push({from: v, to: _path, name: _fileName})
      })
      return new Promise((resolve) => {
        if (_downloadTasks.length === 0) { resolve() }
        downloadRecursion(0)
        // 下载·递归
        function downloadRecursion (index) {
          downloadFileV3(_downloadTasks[index]).then(() => {
            _this.result += `下载(${index + 1}) ${_downloadTasks[index].name} 成功` + '\n'
          }).catch(() => {
            _this.result += `下载(${index + 1}) ${_downloadTasks[index].name} 失败` + '\n'
          }).finally(() => {
            if (index < (_downloadTasks.length - 1)) {
              downloadRecursion(index + 1)
            } else { resolve()}
          })
        }
      })
    },
    // 获取待加密的文件列表
    getEncryptFilesList() {
      _encryptFiles = []
      _zipLog = {}
      let coursewaresDir = path.join(this.toPath, this.bookId)
      return readDir(coursewaresDir).then((files) => {
        files.forEach((file) => {
          let fp = path.join(coursewaresDir, file)
          let fpStat = fs.lstatSync(fp)
          // 删除解压的课件包
          if (fpStat.isDirectory()) { delFile(fp) }
          else if (/zip_encrypted$/.test(file)) {
            _zipLog[file] = {updateTime: Date.now()}
          } else {
            _encryptFiles.push({name: file, path: fp})
          }
        })
      }).catch(() => {
        this.result += `获取待加密的文件列表失败` + '\n'
      })
    },
    // 资源加密
    encrypt() {
      let _this = this
      return new Promise((resolve) => {
        if (_encryptFiles.length === 0) { resolve() }
        encryptRecursion(0)
        // 加密·递归
        function encryptRecursion (index) {
          _this.encryptItem(_encryptFiles[index]).then(() => {
            _this.result += `加密(${index + 1}) ${_encryptFiles[index].name} 成功` + '\n'
          }).catch(() => {
            _this.result += `加密(${index + 1}) ${_encryptFiles[index].name} 失败` + '\n'
          }).finally(() => {
            if (index < (_encryptFiles.length - 1)) {
              encryptRecursion(index + 1)
            } else { resolve()}
          })
        }
      })
    },
    encryptItem (item) {
      return new Promise((resolve, reject) => {
        fs.readFile(item.path, (err, data) => {
          if (err) {
            this.result += `读取文件失败: ${item.name}` + '\n'
            reject()
          } else {
            let encrypted = Buffer.from(data)
            for (let i = 0; i < 10; i++) {
              encrypted[i] = encrypted[i] ^ 0xFB
            }
            writeFileSync(item.path, encrypted)
            resolve()
          }
        })
      })
    },
    // 保存zipLog
    writeZipLog () {
      let _toPath = path.join(this.toPath, this.bookId, 'log.json')
      writeFileSync(_toPath, JSON.stringify(_zipLog))
      _zipLog = {}
    },
    // 生成base.data
    writeBaseData () {
      let _toPath = path.join(this.toPath, this.bookId, 'base.data')
      let _dataStr = JSON.stringify(this.chapterCourseware)
      let _dataBuf = Buffer.from(_dataStr)
      let _dataBufLen = _dataBuf.length
      for (let i = 0; i < _dataBufLen; i++) {
        _dataBuf[i] = _dataBuf[i] ^ 0xFB
      }
      writeFileSync(_toPath, _dataBuf)
    },
    // 生成disk.disc
    writeDiskDisc () {
      if (!this.uDisk) { return this.$toast('请选择目标磁盘后再试') }
      let _toPath = path.join(this.toPath, this.bookId, 'disk.disc')
      let _dataStr = JSON.stringify({
        jsxfDisk: this.uDisk,
        jsxfDate: this.uDate ? +new Date(this.uDate) : Date.now(),
      })
      let _dataBuf = Buffer.from(_dataStr)
      let _dataBufLen = _dataBuf.length
      for (let i = 0; i < _dataBufLen; i++) {
        _dataBuf[i] = _dataBuf[i] ^ 0xFB
      }
      writeFileSync(_toPath, _dataBuf)
      this.$toast(`文件已生成，位于 ${_toPath}`)
    },
    // 校验disk.disc·用于京师书法程序
    checkDiskDisc() {
      // disk.disc内容
      let diskDiscPath = path.join(this.toPath, this.bookId, 'disk.disc')
      let diskDiscInfo = {}
      // 当前程序所在磁盘信息
      let baseDataPath = path.join(this.toPath, this.bookId, 'base.data')
      let baseDataInfo = fs.statSync(baseDataPath)
      let baseDataDiskCode = baseDataPath.slice(0, 2).toUpperCase()
      let command = 'chcp 65001 & wmic logicaldisk get Caption,VolumeSerialNumber /format:list'
      return new Promise((resolve, reject) => {
        if (!fs.existsSync(diskDiscPath)) {return reject('资源文件不存在')}
        try {
          let data = fs.readFileSync(diskDiscPath)
          let len = data.length
          for (let i = 0; i < len; i++) { data[i] = data[i] ^ 0xFB }
          diskDiscInfo = jsonParse(data.toString())
          exec(command, {encoding: 'buffer'}, (error, stdout, stderr) => {
            if (error) { return reject('命令执行失败，请以管理员身份运行程序后重试') }
            console.error(`stderr: ${stderr}`)
            let str = iconv.decode(stdout, 'utf8') || ''
            let lst = str.match(/.*=.*/g)
            let result = []
            let count = {}
            lst && lst.forEach(v => {
              let arr = v.split('=')
              arr = arr.map(v => (v || '').trim())
              if (!count[arr[0]] && count[arr[0]] !== 0) {count[arr[0]] = 0}
              else {count[arr[0]] = count[arr[0]] + 1}
              let index = count[arr[0]]
              if (!result[index]) {result[index] = {}}
              result[index][arr[0]] = arr[1]
            })
            result = result.map(v => {
              return {
                label: (v.Caption || '').toUpperCase(),
                value: v.VolumeSerialNumber
              }
            })
            let diskList = result.filter(v => v.value)
            let diskItem = diskList.filter(v => v.label === baseDataDiskCode)[0] || {}
            let timeDiff = diskDiscInfo.jsxfDate - baseDataInfo.ctimeMs
            if (diskItem.value === diskDiscInfo.jsxfDisk && timeDiff > 0 && timeDiff < 2592000000) {
              resolve(true) // 通过
            } else {
              resolve(false) // 不通过
            }
          })
        } catch (error) {
          reject('读取资源文件失败')
        }
      })
    },
    // 响应数据预处理
    ajaxCheck (response) {
      response.data = response.data || {}
      if (response.status !== 200) {
        this.result += response.statusText + '\n'
        return ''
      }
      if (response.data.code !== 0) {
        this.result += response.data.msg + '\n'
        return ''
      }
      return response.data.data
    },
    // 表单校验
    formCheck () {
      let result = true
      if (!this.bookId) {
        this.result += '课本ID不能为空' + '\n'
        result = false
      }
      if (!this.token) {
        this.result += 'Token不能为空' + '\n'
        result = false
      }
      return result
    },
    async downloadHandler () {
      this.result = ''
      if (!this.formCheck()) return
      this.result = '开始下载...' + '\n'
      this.loadingCount++
      this.result += '开始获取课本所有资源的下载地址...' + '\n'
      await this.getAllFileUrl()
      this.result += `开始下载资源(共计${this.allFileUrl.length}个文件)...` + '\n'
      await this.downloadAllFile()
      this.result += '下载完成' + '\n'
      this.result += '获取待加密的文件列表...' + '\n'
      await this.getEncryptFilesList()
      this.result += '开始加密...' + '\n'
      await this.encrypt()
      this.result += '加密完成' + '\n'
      this.result += '生成课件包日志文件...' + '\n'
      this.writeZipLog()
      this.result += '生成完成' + '\n'
      this.result += '获取课本章节及课件...' + '\n'
      await this.getChapterCourseware()
      this.result += '生成课本基础文件...' + '\n'
      this.writeBaseData()
      this.result += '生成完成' + '\n'
      this.loadingCount--
    }
  }
}
</script>

<style scoped lang="less">
@import "~@/assets/css/_mixins.less";
@import "~@/assets/css/_color.less";
.row-flex {
  & + .row-flex {
    margin-top: .5em;
  }
  .row-flex-prefix {
    width: 6em;
    text-align: right;
  }
}
.form {
  height: calc(100vh - 137px);
  display: flex;
  flex-direction: column;
  .form-group-log {
    margin-top: .5em;
    height: 0;
    flex-grow: 1;
    fieldset {
      height: 100%;
      .fieldset-content {
        height: calc(100% - 2em);
        textarea {
          height: 100%;
        }
      }
    }
  }
  .x-button {
    min-width: 8em;
  }
}
</style>
